#ifndef RETAIN_MCU_H
#define RETAIN_MCU_H

#include <Arduino.h>

#ifdef FREE_RTOS
#include <STM32FreeRTOS.h>
#endif

#include <core_debug.h>
#include <plc_retain.h>

typedef unsigned int u32;
typedef unsigned char u8;

unsigned int GenerateCRC32Sum(const void *buf, unsigned int len, unsigned int init);

void saveRetain(void const *arg);

void Mcu_erase_blocks(u32 PageAddress, u8 NbPages);

void Mcu_Write_flash(u32 adr, const char *buf, int len);

class retain_mcu : public plc_retain {
public:
    int begin(u32 retainSize, u32 adr1, u32 adr2, const u8 *hash, u32 hash_size, u8 thread = 1) {
        this->retain_size = 4 * ((retainSize + 3) / 4);
        this->adr1 = adr1;
        this->adr2 = adr2;
        this->hash = hash;
        this->hash_size = hash_size;
        if (retain_size > 0) {
            buf = (uint8_t *)malloc(retain_size);
            memset(buf, 0, retain_size);
        }
        /* Calc header offset.  */
        header_offset = sizeof(retain_size) + sizeof(hash_size) + hash_size;

        /*  Set header CRC initial state.  */
        header_crc = 0;

        /* Calc crc for header.  */
        header_crc = GenerateCRC32Sum(&retain_size, sizeof(retain_size), header_crc);

        header_crc = GenerateCRC32Sum(&hash_size, sizeof(hash_size), header_crc);

        header_crc = GenerateCRC32Sum(hash, hash_size, header_crc);
#ifdef FREE_RTOS
        if (thread)
        {
            osThreadDef(retain_thd, saveRetain, osPriorityLow, 0, 384); //256不够
            retain_thread = osThreadCreate(osThread(retain_thd), this);
        }
#endif
return 0;
    }

    int close() {
#ifdef FREE_RTOS
        osThreadTerminate(retain_thread);
#endif
        free(buf);
        return 0;
    }

    int check() {
        if (!retain_size)
            return 0;
        core_debug("retain size = %d", retain_size);
        /* Check latest retain file.  */
        if (CheckRetainFile(adr1))
            return 1;

        /* Check if we have backup.  */
        if (CheckRetainFile(adr2))
            return 1;
        core_debug("No valid retain buffer.");
        /* We don't have any valid retain buffer - nothing to remind.  */
        return 0;
    }

    int CheckRetainFile(u32 RETAIN_ADDR) {

        if (CheckFileCRC(RETAIN_ADDR))
            if (CheckFilehash(RETAIN_ADDR)) {
                memcpy(buf, (void *)(RETAIN_ADDR + sizeof(retain_size) + sizeof(hash_size) + hash_size), retain_size);
                return 1;
            }
        return 0;
    }

    int CheckFileCRC(u32 RETAIN_ADDR) {
        /* Set the magic constant for one-pass CRC calc according to ZIP CRC32.  */
        const uint32_t magic_number = 0x2144df1c;

        /* CRC initial state.  */
        uint32_t calc_crc32 = 0;
        char data_block = 0;

        uint32_t adr = RETAIN_ADDR;
        while (adr < RETAIN_ADDR + sizeof(retain_size) + sizeof(hash_size) + hash_size + retain_size + sizeof(int)) {
            data_block = *(u8 *)(adr);
            adr++;
            calc_crc32 = GenerateCRC32Sum(&data_block, sizeof(char), calc_crc32);
        }

        /* Compare crc result with a magic number.  */
        return (calc_crc32 == magic_number) ? 1 : 0;
    }

    int CheckFilehash(u32 RETAIN_ADDR) {

        uint32_t k;
        int offset = sizeof(retain_size);
        uint32_t size;
        memcpy((uint8_t *)&size, (void *)(RETAIN_ADDR + offset), sizeof(size));
        if (size != hash_size)
            return 0;
        for (k = 0; k < hash_size; k++) {
            uint8_t file_digit;
            file_digit = *(u8 *)(RETAIN_ADDR + offset + sizeof(size) + k);
            if (file_digit != *(hash + k))
                return 0;
        }
        return 1;
    }

    void _saveRetain(int loop = 1) {
#ifdef FREE_RTOS
        if (loop)
            vTaskDelay(1000);
#endif
        last_crc = GenerateCRC32Sum(buf, retain_size, header_crc);

        while (1) {
            curr_crc = GenerateCRC32Sum(buf, retain_size, header_crc);
            if ((curr_crc != last_crc) || (!loop)) {
                int adr = adr1;
                Mcu_erase_blocks(adr, 1);
#ifdef FREE_RTOS
                vTaskDelay(10);
#endif
                Mcu_Write_flash(adr, (const char *)&retain_size, sizeof(retain_size));
                adr += sizeof(retain_size);
                Mcu_Write_flash(adr, (const char *)&hash_size, sizeof(hash_size));
                adr += sizeof(hash_size);
                Mcu_Write_flash(adr, (const char *)hash, hash_size);
                adr += hash_size;
                Mcu_Write_flash(adr, (const char *)buf, retain_size);
                adr += retain_size;
                curr_crc = GenerateCRC32Sum(buf, retain_size, header_crc);
                Mcu_Write_flash(adr, (const char *)&curr_crc, sizeof(uint32_t));
                adr = adr2;
                Mcu_erase_blocks(adr, 1);
#ifdef FREE_RTOS
                vTaskDelay(10);
#endif
                Mcu_Write_flash(adr, (const char *)&retain_size, sizeof(retain_size));
                adr += sizeof(retain_size);
                Mcu_Write_flash(adr, (const char *)&hash_size, sizeof(hash_size));
                adr += sizeof(hash_size);
                Mcu_Write_flash(adr, (const char *)hash, hash_size);
                adr += hash_size;
                Mcu_Write_flash(adr, (const char *)buf, retain_size);
                adr += retain_size;
                curr_crc = GenerateCRC32Sum(buf, retain_size, header_crc);
                Mcu_Write_flash(adr, (const char *)&curr_crc, sizeof(uint32_t));
                last_crc = curr_crc;
            }
#ifdef FREE_RTOS
            if (loop)
                vTaskDelay(5000);
            else
#endif
            break;
        }
    }

    void reset() {
        Mcu_erase_blocks(adr1, retain_size / 512 + 1);
        Mcu_erase_blocks(adr2, retain_size / 512 + 1);
    }

    void remind(unsigned int offset, unsigned int count, void *p) {
        memcpy(p, buf + offset, count);
    }

    void retain(unsigned int offset, unsigned int count, void *p) {
        // if (memcmp(buf + offset, p, count) != 0)
        {
            memcpy(buf + offset, p, count);
        }
    }
#define CRC32_POLYNOMIAL                        ((u32)0xEDB88320)
#define RCC_CRC_BIT                             ((u32)0x00001000)


/**
 * @brief  Calc CRC32 for data in bytes
 * @param  pData Buffer pointer
 * @param  uLen  Buffer Length
 * @retval CRC32 Checksum
 */
    u32 CRC32_ForBytes(u8 *pData,u32 uLen) {
        u32 uIndex= 0,uData = 0,i;
        uIndex = uLen >> 2;

        __HAL_RCC_CRC_CLK_ENABLE();

        /* Reset CRC generator */
        CRC->CR = CRC_CR_RESET;

        while(uIndex--) {
#ifdef USED_BIG_ENDIAN
            uData = __REV((u32*)pData);
#else
            ((u8 *)&uData)[0] = pData[0];
            ((u8 *)&uData)[1] = pData[1];
            ((u8 *)&uData)[2] = pData[2];
            ((u8 *)&uData)[3] = pData[3];
#endif
            pData += 4;
            uData = revbit(uData);
            CRC->DR = uData;
        }
        uData = revbit(CRC->DR);
        uIndex = uLen & 0x03;
        while(uIndex--) {
            uData ^= (u32)*pData++;
            for(i = 0; i < 8; i++)
                if (uData & 0x1)
                    uData = (uData >> 1) ^ CRC32_POLYNOMIAL;
                else
                    uData >>= 1;
        }

        __HAL_RCC_CRC_CLK_DISABLE();

        return uData^0xFFFFFFFF;
    }

    static u32 revbit(u32 uData) {
        u32 uRevData = 0,uIndex = 0;
        uRevData |= ((uData >> uIndex) & 0x01);
        for(uIndex = 1; uIndex < 32; uIndex++) {
            uRevData <<= 1;
            uRevData |= ((uData >> uIndex) & 0x01);
        }
        return uRevData;
    }

    unsigned int GenerateCRC32Sum(const void *bytes, unsigned int len, unsigned int init) {
        uint32_t crc = ~init;
        crc=CRC32_ForBytes((u8*)bytes,len);
        return crc;
    }
private:
    u8 current;
    u32 adr1, adr2;
    u32 retain_size;
    u32 hash_size;
    const u8 *hash;
    u32 header_offset;
    u32 header_crc;
    u8 *buf;
#ifdef FREE_RTOS
    osThreadId retain_thread;
#endif
    uint32_t last_crc;
    uint32_t curr_crc;

    static void saveRetain(void const *arg) {
        retain_mcu *rtn = (retain_mcu *)arg;
        rtn->_saveRetain();
    }
};

#endif